// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/gpu/browser_gpu_channel_host_factory.h"

#include "base/bind.h"
#include "base/command_line.h"
#include "base/location.h"
#include "base/profiler/scoped_tracker.h"
#include "base/single_thread_task_runner.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "content/browser/gpu/browser_gpu_memory_buffer_manager.h"
#include "content/browser/gpu/gpu_data_manager_impl.h"
#include "content/browser/gpu/gpu_process_host.h"
#include "content/browser/gpu/shader_cache_factory.h"
#include "content/common/child_process_host_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/gpu_data_manager.h"
#include "content/public/common/content_client.h"
#include "gpu/command_buffer/service/gpu_switches.h"
#include "gpu/ipc/common/gpu_messages.h"
#include "ipc/ipc_channel_handle.h"
#include "ipc/message_filter.h"
#include "services/service_manager/runner/common/client_util.h"

namespace content {

BrowserGpuChannelHostFactory* BrowserGpuChannelHostFactory::instance_ = NULL;

class BrowserGpuChannelHostFactory::EstablishRequest
    : public base::RefCountedThreadSafe<EstablishRequest> {
public:
    static scoped_refptr<EstablishRequest> Create(int gpu_client_id,
        uint64_t gpu_client_tracing_id,
        int gpu_host_id);
    void Wait();
    void Cancel();

    int gpu_host_id() { return gpu_host_id_; }
    IPC::ChannelHandle& channel_handle() { return channel_handle_; }
    gpu::GPUInfo gpu_info() { return gpu_info_; }

private:
    friend class base::RefCountedThreadSafe<EstablishRequest>;
    explicit EstablishRequest(int gpu_client_id,
        uint64_t gpu_client_tracing_id,
        int gpu_host_id);
    ~EstablishRequest() { }
    void EstablishOnIO();
    void OnEstablishedOnIO(const IPC::ChannelHandle& channel_handle,
        const gpu::GPUInfo& gpu_info);
    void FinishOnIO();
    void FinishOnMain();

    base::WaitableEvent event_;
    const int gpu_client_id_;
    const uint64_t gpu_client_tracing_id_;
    int gpu_host_id_;
    bool reused_gpu_process_;
    IPC::ChannelHandle channel_handle_;
    gpu::GPUInfo gpu_info_;
    bool finished_;
    scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
};

scoped_refptr<BrowserGpuChannelHostFactory::EstablishRequest>
BrowserGpuChannelHostFactory::EstablishRequest::Create(
    int gpu_client_id,
    uint64_t gpu_client_tracing_id,
    int gpu_host_id)
{
    scoped_refptr<EstablishRequest> establish_request = new EstablishRequest(gpu_client_id, gpu_client_tracing_id, gpu_host_id);
    scoped_refptr<base::SingleThreadTaskRunner> task_runner = BrowserThread::GetTaskRunnerForThread(BrowserThread::IO);
    // PostTask outside the constructor to ensure at least one reference exists.
    task_runner->PostTask(
        FROM_HERE,
        base::Bind(&BrowserGpuChannelHostFactory::EstablishRequest::EstablishOnIO,
            establish_request));
    return establish_request;
}

BrowserGpuChannelHostFactory::EstablishRequest::EstablishRequest(
    int gpu_client_id,
    uint64_t gpu_client_tracing_id,
    int gpu_host_id)
    : event_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
        base::WaitableEvent::InitialState::NOT_SIGNALED)
    , gpu_client_id_(gpu_client_id)
    , gpu_client_tracing_id_(gpu_client_tracing_id)
    , gpu_host_id_(gpu_host_id)
    , reused_gpu_process_(false)
    , finished_(false)
    , main_task_runner_(base::ThreadTaskRunnerHandle::Get())
{
}

void BrowserGpuChannelHostFactory::EstablishRequest::EstablishOnIO()
{
    // TODO(pkasting): Remove ScopedTracker below once crbug.com/477117 is fixed.
    tracked_objects::ScopedTracker tracking_profile(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "477117 "
            "BrowserGpuChannelHostFactory::EstablishRequest::EstablishOnIO"));
    GpuProcessHost* host = GpuProcessHost::FromID(gpu_host_id_);
    if (!host) {
        host = GpuProcessHost::Get(GpuProcessHost::GPU_PROCESS_KIND_SANDBOXED);
        if (!host) {
            LOG(ERROR) << "Failed to launch GPU process.";
            FinishOnIO();
            return;
        }
        gpu_host_id_ = host->host_id();
        reused_gpu_process_ = false;
    } else {
        if (reused_gpu_process_) {
            // We come here if we retried to establish the channel because of a
            // failure in ChannelEstablishedOnIO, but we ended up with the same
            // process ID, meaning the failure was not because of a channel error,
            // but another reason. So fail now.
            LOG(ERROR) << "Failed to create channel.";
            FinishOnIO();
            return;
        }
        reused_gpu_process_ = true;
    }

    bool preempts = true;
    bool allow_view_command_buffers = true;
    bool allow_real_time_streams = true;
    host->EstablishGpuChannel(
        gpu_client_id_, gpu_client_tracing_id_, preempts,
        allow_view_command_buffers, allow_real_time_streams,
        base::Bind(
            &BrowserGpuChannelHostFactory::EstablishRequest::OnEstablishedOnIO,
            this));
}

void BrowserGpuChannelHostFactory::EstablishRequest::OnEstablishedOnIO(
    const IPC::ChannelHandle& channel_handle,
    const gpu::GPUInfo& gpu_info)
{
    if (!channel_handle.mojo_handle.is_valid() && reused_gpu_process_) {
        // We failed after re-using the GPU process, but it may have died in the
        // mean time. Retry to have a chance to create a fresh GPU process.
        DVLOG(1) << "Failed to create channel on existing GPU process. Trying to "
                    "restart GPU process.";
        EstablishOnIO();
    } else {
        channel_handle_ = channel_handle;
        gpu_info_ = gpu_info;
        FinishOnIO();
    }
}

void BrowserGpuChannelHostFactory::EstablishRequest::FinishOnIO()
{
    event_.Signal();
    main_task_runner_->PostTask(
        FROM_HERE,
        base::Bind(&BrowserGpuChannelHostFactory::EstablishRequest::FinishOnMain,
            this));
}

void BrowserGpuChannelHostFactory::EstablishRequest::FinishOnMain()
{
    if (!finished_) {
        BrowserGpuChannelHostFactory* factory = BrowserGpuChannelHostFactory::instance();
        factory->GpuChannelEstablished();
        finished_ = true;
    }
}

void BrowserGpuChannelHostFactory::EstablishRequest::Wait()
{
    DCHECK(main_task_runner_->BelongsToCurrentThread());
    {
        // TODO(vadimt): Remove ScopedTracker below once crbug.com/125248 is fixed.
        tracked_objects::ScopedTracker tracking_profile(
            FROM_HERE_WITH_EXPLICIT_FUNCTION(
                "125248 BrowserGpuChannelHostFactory::EstablishRequest::Wait"));

        // We're blocking the UI thread, which is generally undesirable.
        // In this case we need to wait for this before we can show any UI
        // /anyway/, so it won't cause additional jank.
        // TODO(piman): Make this asynchronous (http://crbug.com/125248).
        TRACE_EVENT0("browser",
            "BrowserGpuChannelHostFactory::EstablishGpuChannelSync");
        base::ThreadRestrictions::ScopedAllowWait allow_wait;
        event_.Wait();
    }
    FinishOnMain();
}

void BrowserGpuChannelHostFactory::EstablishRequest::Cancel()
{
    DCHECK(main_task_runner_->BelongsToCurrentThread());
    finished_ = true;
}

void BrowserGpuChannelHostFactory::CloseChannel()
{
    DCHECK(instance_);
    if (instance_->gpu_channel_) {
        instance_->gpu_channel_->DestroyChannel();
        instance_->gpu_channel_ = nullptr;
    }
}

bool BrowserGpuChannelHostFactory::CanUseForTesting()
{
    return GpuDataManager::GetInstance()->GpuAccessAllowed(NULL);
}

void BrowserGpuChannelHostFactory::Initialize(bool establish_gpu_channel)
{
    DCHECK(!instance_);
    instance_ = new BrowserGpuChannelHostFactory();
    if (establish_gpu_channel) {
        instance_->EstablishGpuChannel(gpu::GpuChannelEstablishedCallback());
    }
}

void BrowserGpuChannelHostFactory::Terminate()
{
    DCHECK(instance_);
    delete instance_;
    instance_ = NULL;
}

BrowserGpuChannelHostFactory::BrowserGpuChannelHostFactory()
    : gpu_client_id_(ChildProcessHostImpl::GenerateChildProcessUniqueId())
    , gpu_client_tracing_id_(ChildProcessHost::kBrowserTracingProcessId)
    , shutdown_event_(new base::WaitableEvent(
          base::WaitableEvent::ResetPolicy::MANUAL,
          base::WaitableEvent::InitialState::NOT_SIGNALED))
    , gpu_memory_buffer_manager_(
          new BrowserGpuMemoryBufferManager(gpu_client_id_,
              gpu_client_tracing_id_))
    , gpu_host_id_(0)
{
    if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
            switches::kDisableGpuShaderDiskCache)) {
        DCHECK(GetContentClient());
        base::FilePath cache_dir = GetContentClient()->browser()->GetShaderDiskCacheDirectory();
        if (!cache_dir.empty()) {
            GetIOThreadTaskRunner()->PostTask(
                FROM_HERE,
                base::Bind(
                    &BrowserGpuChannelHostFactory::InitializeShaderDiskCacheOnIO,
                    gpu_client_id_, cache_dir));
        }
    }
}

BrowserGpuChannelHostFactory::~BrowserGpuChannelHostFactory()
{
    DCHECK(IsMainThread());
    if (pending_request_.get())
        pending_request_->Cancel();
    for (size_t n = 0; n < established_callbacks_.size(); n++)
        established_callbacks_[n].Run(nullptr);
    shutdown_event_->Signal();
    if (gpu_channel_) {
        gpu_channel_->DestroyChannel();
        gpu_channel_ = NULL;
    }
}

bool BrowserGpuChannelHostFactory::IsMainThread()
{
    return BrowserThread::CurrentlyOn(BrowserThread::UI);
}

scoped_refptr<base::SingleThreadTaskRunner>
BrowserGpuChannelHostFactory::GetIOThreadTaskRunner()
{
    return BrowserThread::GetTaskRunnerForThread(BrowserThread::IO);
}

std::unique_ptr<base::SharedMemory>
BrowserGpuChannelHostFactory::AllocateSharedMemory(size_t size)
{
    std::unique_ptr<base::SharedMemory> shm(new base::SharedMemory());
    if (!shm->CreateAnonymous(size))
        return std::unique_ptr<base::SharedMemory>();
    return shm;
}

void BrowserGpuChannelHostFactory::EstablishGpuChannel(
    const gpu::GpuChannelEstablishedCallback& callback)
{
    DCHECK(!service_manager::ServiceManagerIsRemote());
    if (gpu_channel_.get() && gpu_channel_->IsLost()) {
        DCHECK(!pending_request_.get());
        // Recreate the channel if it has been lost.
        gpu_channel_->DestroyChannel();
        gpu_channel_ = NULL;
    }

    if (!gpu_channel_.get() && !pending_request_.get()) {
        // We should only get here if the context was lost.
        pending_request_ = EstablishRequest::Create(
            gpu_client_id_, gpu_client_tracing_id_, gpu_host_id_);
    }

    if (!callback.is_null()) {
        if (gpu_channel_.get())
            callback.Run(gpu_channel_);
        else
            established_callbacks_.push_back(callback);
    }
}

// Blocking the UI thread to open a GPU channel is not supported on Android.
// (Opening the initial channel to a child process involves handling a reply
// task on the UI thread first, so we cannot block here.)
scoped_refptr<gpu::GpuChannelHost>
BrowserGpuChannelHostFactory::EstablishGpuChannelSync()
{
#if defined(OS_ANDROID)
    NOTREACHED();
    return nullptr;
#endif
    EstablishGpuChannel(gpu::GpuChannelEstablishedCallback());

    if (pending_request_.get())
        pending_request_->Wait();

    return gpu_channel_;
}

gpu::GpuMemoryBufferManager*
BrowserGpuChannelHostFactory::GetGpuMemoryBufferManager()
{
    return gpu_memory_buffer_manager_.get();
}

gpu::GpuChannelHost* BrowserGpuChannelHostFactory::GetGpuChannel()
{
    if (gpu_channel_.get() && !gpu_channel_->IsLost())
        return gpu_channel_.get();

    return NULL;
}

void BrowserGpuChannelHostFactory::GpuChannelEstablished()
{
    DCHECK(IsMainThread());
    DCHECK(pending_request_.get());
    if (!pending_request_->channel_handle().mojo_handle.is_valid()) {
        DCHECK(!gpu_channel_.get());
    } else {
        // TODO(robliao): Remove ScopedTracker below once https://crbug.com/466866
        // is fixed.
        tracked_objects::ScopedTracker tracking_profile1(
            FROM_HERE_WITH_EXPLICIT_FUNCTION(
                "466866 BrowserGpuChannelHostFactory::GpuChannelEstablished1"));
        GetContentClient()->SetGpuInfo(pending_request_->gpu_info());
        gpu_channel_ = gpu::GpuChannelHost::Create(
            this, gpu_client_id_, pending_request_->gpu_info(),
            pending_request_->channel_handle(), shutdown_event_.get(),
            gpu_memory_buffer_manager_.get());
    }
    gpu_host_id_ = pending_request_->gpu_host_id();
    pending_request_ = NULL;

    // TODO(robliao): Remove ScopedTracker below once https://crbug.com/466866 is
    // fixed.
    tracked_objects::ScopedTracker tracking_profile2(
        FROM_HERE_WITH_EXPLICIT_FUNCTION(
            "466866 BrowserGpuChannelHostFactory::GpuChannelEstablished2"));

    std::vector<gpu::GpuChannelEstablishedCallback> established_callbacks;
    established_callbacks_.swap(established_callbacks);
    for (auto& callback : established_callbacks)
        callback.Run(gpu_channel_);
}

// static
void BrowserGpuChannelHostFactory::AddFilterOnIO(
    int host_id,
    scoped_refptr<IPC::MessageFilter> filter)
{
    DCHECK_CURRENTLY_ON(BrowserThread::IO);

    GpuProcessHost* host = GpuProcessHost::FromID(host_id);
    if (host)
        host->AddFilter(filter.get());
}

// static
void BrowserGpuChannelHostFactory::InitializeShaderDiskCacheOnIO(
    int gpu_client_id,
    const base::FilePath& cache_dir)
{
    GetShaderCacheFactorySingleton()->SetCacheInfo(gpu_client_id, cache_dir);
}

} // namespace content
